.com
Hosted by:
Unit testing expertise at your fingertips!
Home | Discuss | Lists

Test Code Duplication

The book has now been published and the content of this chapter has likely changed substanstially.
Please see page 213 of xUnit Test Patterns for the latest information.

The same test code is repeated many times.

Many of the tests in a suite will need to do similar things. Often, tests exercise scenarios that are a variation on a theme. Tests may require similar fixture setup or result verification logic. In some cases, even the exercise SUT phase of the test involves non-trivial logic.

The need for tests to do similar things often results in Test Code Duplication

Symptoms

Several tests may contain a common subset of essentially the same statements.

   public void testInvoice_addOneLineItem_quantity1_b() {
      // Exercise
      inv.addItemQuantity(product, QUANTITY);
      // Verify
      List lineItems = inv.getLineItems();
      assertEquals("number of items", lineItems.size(), 1);
      // Verify only item
      LineItem expItem = new LineItem(inv, product, QUANTITY);
      LineItem actual = (LineItem)lineItems.get(0);
      assertEquals(expItem.getInv(), actual.getInv());
      assertEquals(expItem.getProd(), actual.getProd());
      assertEquals(expItem.getQuantity(), actual.getQuantity());
   }
  
  
   public void testRemoveLineItemsForProduct_oneOfTwo() {
      // setup:
      Invoice inv = createAnonInvoice();
      inv.addItemQuantity(product, QUANTITY);
      inv.addItemQuantity(anotherProduct, QUANTITY);
      LineItem expItem = new LineItem(inv, product, QUANTITY);
      // Exercise
      inv.removeLineItemForProduct(anotherProduct);
      // Verify
      List lineItems = inv.getLineItems();
      assertEquals("number of items", lineItems.size(), 1);
      LineItem actual = (LineItem)lineItems.get(0);
      assertEquals(expItem.getInv(), actual.getInv());
      assertEquals(expItem.getProd(), actual.getProd());
      assertEquals(expItem.getQuantity(), actual.getQuantity());
   }
Example TestCodeDuplicationAcrossMethods embedded from java/com/clrstream/camug/example/test/InvoiceTest.java

A single test may contain repeated groups of similar statements.

   public void testInvoice_addTwoLineItems_sameProduct() {
      Invoice inv = createAnonInvoice();
      LineItem expItem1 = new LineItem(inv, product, QUANTITY1);
      LineItem expItem2 = new LineItem(inv, product, QUANTITY2);
      // Exercise
      inv.addItemQuantity(product, QUANTITY1);
      inv.addItemQuantity(product, QUANTITY2);
      // Verify
      List lineItems = inv.getLineItems();
      assertEquals("number of items", lineItems.size(), 2);
      //   verify first item
      LineItem actual = (LineItem)lineItems.get(0);
      assertEquals(expItem1.getInv(), actual.getInv());
      assertEquals(expItem1.getProd(), actual.getProd());
      assertEquals(expItem1.getQuantity(), actual.getQuantity());
      //   verify second item
      actual = (LineItem)lineItems.get(1);
      assertEquals(expItem2.getInv(), actual.getInv());
      assertEquals(expItem2.getProd(), actual.getProd());
      assertEquals(expItem2.getQuantity(), actual.getQuantity());
   }
Example TestCodeDuplicationWithinMethod embedded from java/com/clrstream/camug/example/test/InvoiceTest.java

Both of these examples exhibit Test Code Duplication that is easily noticed. It is harder to notice the duplication if it occurs across Test Method (page X) that are in different Testcase Classes (page X).

Impact

"Cut and paste" often results in many copies of the same code. This code must be maintained every time the system under test (SUT) is modified in a way that affects the semantics (number of arguments, argument attributes, returned object attributes, calling sequences) of its methods. This can result in a very large increase in the cost to introduce new functionality (High Test Maintenance Cost (page X)) because of the effort involved in updating all the tests that have copies of the affected code.

Causes

Cause: Cut-and-Paste Code Reuse

"Cut and Paste" is a powerful tool for writing code fast but it results in many copies of the same code each of which must be maintained in parallel.

Root Cause

Cut-and-Paste Code Reuse is often the default way to reuse logic. Developers who focus on details of "how" to do something will often repeat the same code many times because they cannot (or do not take the time to) focus on the big picture (the intent) of the test.

A contributing factor may be a lack of refactoring skills or refactoring experience that keeps them from extracting the big picture from the detailed code they have written. Of course, it may also simply be time pressure that keeps the refactoring from occurring. As a result, test code gets more complicated over time rather than getting simpler.

Possible Solution

Once the Test Code Duplication has occurred, the best solution is to use a Extract Method[Fowler] refactoring to create a Test Utility Method (page X) from one of the examples and then to generalize it to handle each of the copies. When the Test Code Duplication is fixture setup logic, we end up with Creation Methods (page X) or Finder Methods (see Test Utility Method). When the logic is result verification, we end up with Custom Assertions (page X) or Verification Methods (see Custom Assertion).

We can use an Introduce Parameter[JBrains] refactoring to convert any literal constants inside the extracted method into parameters that can be passed in to customize its behavior for each test that calls it.

Test Code Duplication can be avoided to a large degree by writing the Test Methods "outside id" focusing on intent. Whenever we need to do something that involves several lines of code, we simply call a non-existent Test Utility Method to do it. We write all our tests this way and then fill in implementations of the Test Utility Methods to get the tests to compile and run. (Modern IDE's have made this process much easier by providing automatic method skeleton generation at a click of the mouse.)

Cause: Reinventing the Wheel

While Cut and Paste Code Reuse deliberately makes copies of existing code to reduce the effort of writing tests, it is also possible to accidently write the same sequence of statements in different tests.

Root Cause

This is primarily caused by a lack of awareness of what Test Utility Methods are available but it can also be caused by a predisposition to write one's own code rather than reusing coded written by others.

Possible Solution

The technical solution is largely the same as for Cut and Paste Code Reuse but the process solution is somewhat different. The test automater has to look around more to discover what Test Utility Method are available before reinventing the wheel (I.e. writing new code.)

Further Reading

Test Code Duplication was first described in a paper at XP2001 called "Refactoring Test Code" [RTC].



Page generated at Wed Feb 09 16:39:51 +1100 2011

Copyright © 2003-2008 Gerard Meszaros all rights reserved

All Categories
Introductory Narratives
Web Site Instructions
Code Refactorings
Database Patterns
DfT Patterns
External Patterns
Fixture Setup Patterns
Fixture Teardown Patterns
Front Matter
Glossary
Misc
References
Result Verification Patterns
Sidebars
Terminology
Test Double Patterns
Test Organization
Test Refactorings
Test Smells
Test Strategy
Tools
Value Patterns
XUnit Basics
xUnit Members
All "Test Smells"
Code Smells
--Obscure Test
--Conditional Test Logic
--Hard-to-Test Code
--Test Code Duplication
----Cut-and-Paste Code Reuse
----Reinventing the Wheel
--Test Logic in Production
Behavior Smells
--Assertion Roulette
--Erratic Test
--Fragile Test
--Frequent Debugging
--Manual Intervention
--Slow Tests
Project Smells
--Buggy Tests
--Developers Not Writing Tests
--High Test Maintenance Cost
--Production Bugs